#!/usr/bin/env python3

"""
Author: Yanrui Hu
Date: 2023/4/23
Description: 测试在 Ubuntu Server 上面进行容器的部署
Keywords: Ubuntu, Container, Docker, 毕设
Process:
- 调用 podman 进行目标容器镜像的拉取 (require 镜像的URL)
- 使用 podman inspect Image 获取到 diff 层的位置
- 进入 diff 文件夹, 把里面的东西递归复制到当前机器的对应目录中(此过程要十分小心, 细致比对)


-f | --force 表示「强制部署」,
ATTENTION: "force-deploy" will not undo the current deploy and deploy the new image onto it.

未使用此选项时, 是自动部署模式, 在部署新的镜像时, 程序自动分析status.log文件,
计算期望部署情况和当前部署情况的差异(diff_dirs), 如果发现存在一些不必撤销的公共层,
则不会完全撤销上次镜像对应的文件系统的更改, 仅仅撤销必须撤销的一部分, 之后, 继续部署未部署的层,
直到目标镜像部署成功。

注意: 使用此选项可能会造成一些麻烦！
    推荐使用此选项的场景: 想要结合两个不同的镜像的特点。这两个镜像对原始镜像的修改应当是「不相干的」
    比如, 现有 从原始镜像得到的镜像A和B, A与B对于原始镜像修改丝毫不同
    假设我们需要将A与B的修改结合起来, 得到一个新的镜像C
    我们可以在部署A(或者B) 之后, 使用-f选项部署B(或者A), 即可达到与部署C相同的效果
    这种方法可以无限次应用: A + B + C + D + ... = X
    A, B, C, D, ... 都是在一个原始镜像上的修改, 且这些修改之间基本上没有任何关联（否则会出现一些意想不到的效果）
    这有些类似于Git的Feature Branch的概念, 但是这里的「分支」是在文件系统层面上的, 而不是在代码层面上的

"""
import argparse
import os, sys, subprocess
import log
import fileutils
import logging
import re

DESCRIPTION = "将指定镜像对应的文件系统部署在本机上, 或撤销部署"
EPILOG = """Example:
    python3 autodeploy.py  deploy --image='localhost:5000/ubuntu:v4.0'\
        --target-dir='~/test'"""

REPO_PATH = os.path.dirname(os.path.abspath(__file__))

# IMAGE = "docker.io/yanruihu/ubuntu:v2.1"
# IMAGE = "docker.io/yanruihu/ubuntu:manyDir"
IMAGE = "docker.io/yanruihu/ubuntu:v1.0"
TARGET_DIR = "/home/hyr/test/Test0"

# ------------------------------------------------------------
# 配置日志
# ------------------------------------------------------------
# 创建一个 Logger 实例
running_logger = logging.getLogger('running')
running_logger.setLevel(logging.INFO)

# 创建一个 FileHandler 实例
file_handler = logging.FileHandler(f'{REPO_PATH}/running.log')
file_handler.setLevel(logging.INFO)

# 设置日志消息格式
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d %(funcName)s - %(message)s'
)
file_handler.setFormatter(formatter)

# 添加 handler 到 Logger 实例
running_logger.addHandler(file_handler)


# ------------------------------------------------------------
# Follow are some utils:
# ------------------------------------------------------------
def want_continue(quiet=False):
    if quiet:
        return

    res = input("> 是否继续 [Y/n] (default is Y): ")
    if res != "" and res != "Y":
        print("已退出")
        exit(0)


def get_input(prompt, expected: 'list[str]'):
    """获取用户输入, 使用prompt作为提示词, expected 作为期待获取的值的列表
    如若用户输入的值不符合预期, 需要重新输入
    """
    res = input(prompt)

    while res not in expected:
        res = input(prompt)
    return res


def ensure_exist(dirname, quiet=False):
    # 如果 path 不存在, 先创建
    if not os.path.exists(dirname):
        if not quiet:
            print(f"目标文件夹 {dirname} 不存在, 正在创建……")
        os.mkdir(dirname)

    elif not quiet:
        print(f"目标文件夹 {dirname} 确认存在")


def ensure_podman(quiet=False):
    """确保 podman 的存在, 未安装时安装"""
    running_logger.info("ensure_podman 开始检测 podman 是否已安装")
    if not quiet:
        print(
            "本程序使用 Podman 拉取容器镜像, apt-get install podman 需要 root 权限\n"
            "若非 root 用户, 在安装 podman 时需要输入当前用户密码\n"
            "正在检测 Podman 是否已安装……"
        )

    if os.path.exists('/usr/bin/podman'):
        running_logger.info("ensure_podman 检测到已经安装了 podman")
        if not quiet:
            print("Podman 已安装~~~")
        return

    running_logger.info("ensure_podman 检测到尚未安装 podman, 尝试自动安装……")
    if not quiet:
        print("Podman 尚未安装, 正在尝试自动安装……")

    update_cmd = 'update'
    install_cmd = '-y install podman'

    if os.path.exists('/usr/bin/apt-get'):
        update_cmd = 'apt-get ' + update_cmd
        install_cmd = 'apt-get ' + install_cmd
    elif os.path.exists('/usr/bin/dnf'):
        update_cmd = 'dnf ' + update_cmd
        install_cmd = 'dnf ' + install_cmd
    elif os.path.exists('/usr/bin/yum'):
        update_cmd = 'yum ' + update_cmd
        install_cmd = 'yum ' + install_cmd
    elif os.path.exists('/usr/bin/pacman'):
        update_cmd = 'pacman -Syy'
        install_cmd = 'pacman -Sy podman'
    else:
        running_logger.error("ensure_podman 暂时没有实现对当前 Linux 发行版的支持, 请自行安装 podman")
        print("ensure_podman 暂时没有实现对当前 Linux 发行版的支持, 请自行安装 podman", file=sys.stderr)
        exit(1)

    # 确认当前的用户是否为 root
    if os.geteuid() != 0:
        update_cmd = "sudo " + update_cmd
        install_cmd = "sudo " + install_cmd

    res = subprocess.run(update_cmd, shell=True, capture_output=True)
    if res.stdout:
        running_logger.info(res.stdout.decode('utf-8'))
    exit_code = res.returncode
    if exit_code != 0:
        running_logger.error("ensure_podman 更新源失败")
        running_logger.error(res.stderr.decode('utf-8'))
        print("ensure_podman() 更新源失败", file=sys.stderr)
        exit(1)

    res = subprocess.run(install_cmd, shell=True, capture_output=True)
    if res.stdout:
        running_logger.info(res.stdout.decode('utf-8'))
    exit_code = res.returncode
    if exit_code != 0:
        running_logger.error("ensure_podman() 安装 podman 失败")
        running_logger.error(res.stderr.decode('utf-8'))
        print("ensure_podman() 安装 podman 失败", file=sys.stderr)
        exit(1)


def check_image(image, quiet=False):
    """Check if image exist"""
    running_logger.info(f"正在检测镜像 {image} 是否存在……")
    if not quiet:
        print(f"正在检测镜像 {image} 是否存在……")
        # print(f"podman images {image}" " --format '{{.Repository}}:{{.Tag}}'") # for debug
    file = os.popen(f"podman images {image}" " --format '{{.Repository}}:{{.Tag}}' ")
    lines = file.readlines()
    lines = [line.strip() for line in lines]  # 去掉每行末尾的换行符
    running_logger.info(f"检测到{image=} 的信息为: {lines=}")

    if image in lines:
        running_logger.info(f"镜像 {image} 已存在~~~")
        if not quiet:
            print(f"镜像 {image} 已存在~~~")
        return True
    else:
        running_logger.info(f"镜像 {image} 不存在")
        if not quiet:
            print(f"镜像 {image} 不存在")
        return False


def pull(image: str, quiet=False):
    """Pull image by podman"""
    running_logger.info(f"正在拉取镜像 {image}")
    if not quiet:
        print(f"正在拉取镜像 {image}")

    res = subprocess.run(
        f"podman pull {image} --tls-verify=false", shell=True, capture_output=True
    )
    # --tls-verify=false 用于解决私有存储库没有证书的问题
    if res.stdout:
        running_logger.info(res.stdout.decode('utf-8'))
    exit_code = res.returncode

    if exit_code != 0:
        running_logger.error(f"拉取镜像失败! 请手动排查错误并保证 podman 安装")
        running_logger.error(res.stderr.decode('utf-8'))
        print(f"拉取镜像失败! 请手动排查错误并保证 podman 安装", file=sys.stderr)
        exit(1)
    if not quiet:
        running_logger.info(f"拉取镜像成功{image}! ")
        print(f"拉取镜像成功{image}! ")


def ensure_image(image, quiet=False):
    """确保镜像的存在, 不存在时拉取"""
    running_logger.info(f"正在检测镜像 {image} 是否存在")
    if check_image(image, quiet) == False:
        if not quiet:
            print("正在为您拉取最新镜像")
        pull(image)
    elif not quiet:
        res = get_input("> 是否需要从远端获取最新镜像 [Y/n] (default is Y): ", ['Y', 'n', ""])
        if res == "" or res == "Y":
            pull(image)


def prompt():
    """输出程序开始的提示信息"""
    prompt = """欢迎使用 autodeploy 程序, 本程序将帮助您快速部署容器镜像, 支持以下功能:
    1. 自动确认 Podman 是否已安装, 若未安装, 将自动安装
    2. 自动确认目标镜像是否已存在, 若不存在, 将自动拉取
    3. 支持对已经部署的镜像进行 undo 操作（撤销部署）(undo all apply)
    4. 支持在已经部署镜像的情况下, 部署新的镜像. 将会自动分析两者差异, 撤销不需要的镜像层, 部署新的镜像层 (auto deploy IMAGE)
    5. 支持在已经部署镜像的情况下, 强制部署新的镜像, 不会撤销不需要的镜像层。需要用户谨慎操作！(force deploy IMAGE)
    6. 支持撤销部署的最后一层 (undo last apply) 注意！此操作仅适合高级用户, 使用前需要仔细确认。
    7. 支持在初次部署时清空所有历史日志 (clear log) [危险操作！不可撤销！]

    其他特点:
    1. 可以通过修改 autodeploy.py 中的 IMAGE 变量的值来指定镜像；也可以在程序运行过程中临时修改
    2. 可以通过修改 autodeploy.py 中的 TARGET_DIR 变量的值来指定部署目标文件夹；也可以在程序运行过程中临时修改
    """
    print(prompt)

    # 获取当前的部署信息
    print("    当前部署信息:")
    with open(f"{REPO_PATH}/.database/status.log", "r") as f:
        lines = f.readlines()
        for i in range(2):
            print("   ", lines[i].strip())


def confirm_info_before_pull(quiet=False):
    """在拉取镜像前确认信息"""
    if quiet:  # 如果开启了 quiet 模式, 则不需要确认信息
        return

    global IMAGE
    print("请确认即将拉取的 IMAGE: ", IMAGE)
    r = input("> 临时修改 New Image Name (不变更请直接回车): ")
    IMAGE = r if len(r) > 0 else IMAGE


def confirm_info_befor_deploy(quiet=False):
    """在部署前确认信息"""
    if quiet:  # 如果开启了 quiet 模式, 则不需要确认信息
        return

    confirm_info_before_pull(quiet)

    global TARGET_DIR
    print(f"请确认部署的目标 TARGET_DIR: {TARGET_DIR}")
    r = input("> 临时修改 New Target Dir (不变更请直接回车): ")
    TARGET_DIR = r if len(r) > 0 else TARGET_DIR


# ------------------------------------------------------------
# Follow are about info-getting functions:
# ------------------------------------------------------------
def get_status(quiet=False):
    """获取当前的状态信息 (Current Status)"""
    # Open and read the .database/status.log file
    with open(f"{REPO_PATH}/.database/status.log", "r") as f:
        last_modified_time = f.readline()
        current_image_version = f.readline()
        return (last_modified_time, current_image_version)


def get_all_operations(quiet=False):
    """获取所有的操作 (From init)"""
    # 使用 less 打开 ./database/operations.log 文件, 匹配到 APPLY_DIFF_DIR
    os.system(f"less -p APPLY_DIFF_DIR {REPO_PATH}/.database/operations.log")


def get_equivalent_operations(quiet=False):
    """获取当前的状态对应的 等价操作 (多次升级、降级、变更后)"""
    # 使用 less 打开 ./database/status.log, 匹配到 APPLY_DIFF_DIR
    os.system(f"less -p APPLY_DIFF_DIR {REPO_PATH}/.database/status.log")


def get_last_operations(quiet=False):
    """获取最后一次操作 (Last)"""
    os.system(f"less -p APPLY_DIFF_DIR {REPO_PATH}/.database/last-oper.log")


# ------------------------------------------------------------
# DON'T COMPLETE or USE THIS METHOD!
# ------------------------------------------------------------
def undo_diff_dir(dir):
    """本意: undeploy 指定的 diff_dir
    ⚠ 此方法不应该被实现！因为撤销时只允许依次撤销, 不可以随意撤销（会引入混乱）！
    """
    pass


# 反向操作 --> 以一行为单位
def undo_oper(oper_record: str, quiet=False):
    """进行操作的反向操作, 还原更改
        传入的操作需要记录在 .database/operations.log 中

        一些操作的样例如下：
    2023-05-24 10:42:58,120 - Copy file: '/home/hyr/.local/share/containers/storage/overlay/852789db957a1d6e431bf65c0caee85a13dabc4902fe68c0fb9c79e992982f27/diff/file1' -> '/home/hyr/test/file1'
    2023-05-24 10:42:58,126 - Create symlink: '/home/hyr/test/link1' -> 'file1'
    2023-05-24 10:42:58,132 - Make dir: mkdir: created directory '/home/hyr/test/root'
    2023-05-24 10:42:58,137 - Copy file: '/home/hyr/.local/share/containers/storage/overlay/852789db957a1d6e431bf65c0caee85a13dabc4902fe68c0fb9c79e992982f27/diff/root/default.sh' -> '/home/hyr/test/root/default.sh'
    2023-05-24 10:42:58,145 - Make dir: mkdir: created directory '/home/hyr/test/dir1'
    2023-05-24 10:42:58,150 - Create symlink: '/home/hyr/test/dir1/link2' -> '../file1'
    2023-05-24 10:42:58,155 - Create symlink: '/home/hyr/test/dir1/link3' -> '/root/file1'
        下面会使用 startswith 对这些字符串进行处理, 判断类型
        之后, 提取出关键的目录和文件名, 进行反向操作。提取信息可以使用正则表达式
    """

    running_logger.info(f"原始操作数据: {oper_record}")
    oper = oper_record[26:].strip()
    running_logger.info(f"第一次加工后的oper数据: {oper}")
    if oper == '':
        return

    copy_pattern = r"Copy file: '(.*)' -> '(.*)'"
    copy_bak_pat = r"Copy file: '(.*)' -> '(.*)' \(backup: '(.*)'\)"
    move_pattern = r"Move file: renamed '(.*)' -> '(.*)'"
    mkdir_pattern = r"Make dir: mkdir: created directory '(.*)'"
    symlink_create_pat = r"Create symlink: '(.*)' -> '(.*)'"
    symlink_update_pat = r"Update symlink: '(.*)' -> '(.*)' \(old link target: '(.*)'\)"

    # Judge the type of operation
    if oper.startswith("Copy") and "(backup:" not in oper:  # 没有备份的复制，只需要删除目标文件即可
        file1, file2 = re.match(copy_pattern, oper).groups()
        fileutils.remove_file(file2)
    elif oper.startswith("Copy") and "(backup:" in oper:  # 有备份的复制，需要将备份文件移动到目标文件
        file1, file2, bak = re.match(copy_bak_pat, oper).groups()
        fileutils.move_file(bak, file2)

    elif oper.startswith("Move"):  # 这种情况一般不会出现在操作日志中
        file1, file2 = re.match(move_pattern, oper).groups()
        fileutils.move_file(file2, file1)

    elif oper.startswith("Make dir"):  # 创建目录, 需要删除目录
        dir = re.match(mkdir_pattern, oper).group(1)
        fileutils.remove_dir(dir)
    elif oper.startswith("Create symlink"):  # 创建软链接, 需要删除软链接
        linkname, targetname = re.match(symlink_create_pat, oper).groups()
        fileutils.remove_symlink(linkname)
    elif oper.startswith("Update symlink"):  # 更新软链接, 需要恢复软链接
        linkname, targetname, old_targetname = re.match(
            symlink_update_pat, oper
        ).groups()
        fileutils.update_symlink(old_targetname, linkname)
    else:
        running_logger.error(f"Error: 未识别出操作类型 {oper}")
        print(
            f"Error: 未识别出操作类型 {oper_record}\n",
            file=sys.stderr,
        )
    return


# 反向操作 --> 以块为单位
def undo_last_apply(independent=False, quiet=False):
    """取消最后一个 apply_diff_dir.
    Log files which need to be modified:

    - .database/operations.log (add this REVERT_APPLY, instantly logging operations)
    - .database/last-oper.log (remove the last APPLY (or REVERT) block, add this REVERT_APPLY)
    - .database/status.log (remove the last APPLY_DIFF_DIR)

    """

    with open(f"{REPO_PATH}/.database/status.log", 'r') as f:
        lines = f.readlines()
        """ last_modified_time = lines[0]
        current_image_version = lines[1] """
        while lines[-1] == '\n':
            del lines[-1]

        if lines[-1].startswith("END_APPLY"):
            del lines[-1]  # del "END_APPLY\n"
        else:
            if not quiet:
                print("当前不需要任何撤销, status.log 中没有记录任何 APPLY_DIFF_DIR 操作")
            return

        if independent:
            log.clean_log_file('last-oper.log')

        log.append_log_file("last-oper.log", "\nREVERT_APPLY\n")

        for i in range(len(lines) - 1, 1, -1):
            if lines[i].startswith("APPLY_DIFF_DIR"):
                lis = lines[i].split()
                diff_dir = lis[1]
                target_dir = lis[3]

                log.append_log_file(
                    "last-oper.log",
                    f"END_REVERT {diff_dir} -> {target_dir}\n",
                )
                del lines[-1]  # del 'APPLY_'...
                break

            # Handel lines[i] 对每一行进行撤销操作
            undo_oper(lines[i], quiet)

            # info = lines[i][26:].strip()  # '26' 是时间戳以及空格、分隔符的长度
            # pprint(f"info {info}")

            del lines[i]

        while lines[-1] == '\n':
            del lines[-1]

        # 将更改后的内容写入日志文件
        with open(f"{REPO_PATH}/.database/status.log", 'w') as f:
            f.writelines(lines)

    if independent:
        log.update_operations_log()
        log.update_log_meta_info("You_used_`undo_last_apply`_independently.")


# 反向操作 --> 所有块
def undo_all_apply(quiet=False):
    """撤销所有的APPLY_DIFF_DIR操作"""
    cur_diff_dirs = []
    with open(f"{REPO_PATH}/.database/status.log", 'r') as f:
        lines = f.readlines()
        for line in lines:
            if line.startswith("APPLY_DIFF_DIR"):
                cur_diff_dirs.append(line.split()[1].strip("'\""))

    if not quiet:
        print("cur_diff_dirs:", cur_diff_dirs, '\n')

    cur_len = len(cur_diff_dirs)
    if cur_len == 0:
        if not quiet:
            print("当前不需要任何撤销, status.log 中没有记录任何 APPLY_DIFF_DIR 操作")
        return

    # 在 undo 和 deploy 之前, 先将 last-oper.log 中的历史记录清除
    log.clean_log_file("last-oper.log")

    # 更新元信息
    log.update_log_meta_info("null")
    for dir in cur_diff_dirs:
        # undo_diff_dir(dir) # Alert! Arbitrary undo is not allowed.
        undo_last_apply(quiet)  # Just undo and undo again.

    # 更新 operations.log
    log.update_operations_log()

    # 更新status.log
    log.update_status_log(include_undo=True)

    running_logger.info(f"撤销{cur_diff_dirs}完成")
    if not quiet:
        print(f"撤销完成")


# 正向操作 --> 以块为单位
def apply_diff_dir(diff_dir, target_dir="/", quiet=False):
    """deploy 指定的 diff_dir.
    调用方: auto_deploy, force_deploy

    通过调用 copy_recur_0 方法实现
    此方法需要修改的日志文件有:
    - .database/last-oper.log (clear OLD log data, add a new APPLY block)
    - .database/operations.log (add a new APPLY block, instantly logging operations)
    - .database/status.log (add a new APPLY_DIFF_DIR)
    """

    # 在last-oper.log文件的最后一行写上APPLY_DIFF_DIR
    log.append_log_file(
        "last-oper.log", f"\nAPPLY_DIFF_DIR '{diff_dir}' -> '{target_dir}'\n"
    )

    fileutils.copy_recur_0(diff_dir, target_dir, quiet)

    # 完成后在last-oper.log文件的最后一行写上END_APPLY
    log.append_log_file("last-oper.log", "END_APPLY\n")


# 正向操作 --> 强制覆盖
def force_deploy(image, target_dir="/", quiet=False):
    """deploy a new image with current image forcely mixed in"""
    # 这个过程可以这样描述
    # 1. 分析出 image 对应的 diff 文件夹
    # 2. 将 diff 文件夹中的文件复制到目标文件夹中(调用 deploy_diff_dir)

    # 1. 分析出 image 对应的 diff 文件夹
    diff_dirs = log.inspect(image, quiet)

    # 在 deploy 之前, 先将 last-oper.log 中注释之下的内容删除
    log.clean_log_file("last-oper.log")

    # 更新元信息
    log.update_log_meta_info(image)

    for dir in diff_dirs:
        apply_diff_dir(dir, target_dir, quiet)

    # 更新 operations.log
    log.update_operations_log()

    # 更新 status.log
    log.update_status_log(include_undo=False)


# 正向操作 --> 自动分析
def auto_deploy(new_image, target_dir="/", quiet=False):
    """deploy a new image with automatic analysis"""
    # 这个过程可以这样描述
    # 1. 分析出 image 对应的 diff 文件夹, 得到 new_diff_dirs
    # 2. 分析 status.log 中的 APPLY_DIFF_DIR 记录
    #   1. 找到上次部署的目标文件夹, 设为 old_target_dir。如果为空, 说明没有部署过, 这是第一次部署, 则直接部署 new_diff_dirs
    #   2. 如果不为空, 则处理APPLY_DIFF_DIR记录, 获得 cur_diff_dirs
    # 3. 对比 new_diff_dirs 和 cur_diff_dirs, 得到需要 apply 和 revert 的 diff 文件夹
    #   1. 首先是分析 new_diff_dirs 和 cur_diff_dirs 的公共前缀, 设从下标i开始, 两者有区别
    #   2. cur_diff_dirs[i:] 中的 diff 文件夹需要 revert
    #   3. new_diff_dirs[i:] 中的 diff 文件夹需要 apply

    # 1. 分析出 image 对应的 diff 文件夹, 得到 new_diff_dirs
    new_diff_dirs = log.inspect(new_image, quiet)
    # new_diff_dirs = [d + "/" for d in new_diff_dirs] # For debug
    if not quiet:
        print("new_diff_dirs:", new_diff_dirs)

    # 2.1 分析 status.log 中的 APPLY_DIFF_DIR 记录, 得到 old_target_dir
    old_target_dir = log.get_last_deploy_target_dir()
    if old_target_dir is None:  # 为空说明是第一次部署, 直接部署即可
        # 说明当前没有任何 diff 文件夹被 apply, 直接 apply 即可
        # 在 undo 和 deploy 之前, 先将 last-oper.log 中的历史记录清除
        log.clean_log_file("last-oper.log")

        # 更新元信息
        log.update_log_meta_info(new_image)

        for dir in new_diff_dirs:
            apply_diff_dir(dir, target_dir, quiet)

        # 更新 operations.log
        log.update_operations_log()

        # 更新 status.log
        log.update_status_log(include_undo=False)
        return

    elif old_target_dir != target_dir:  # 如果上次部署的目标目录与当前目标目录不一致
        running_logger.error(
            f"Error: old_target_dir({old_target_dir}) != Current target_dir({target_dir})"
        )
        print(
            f"Error: old_target_dir '{old_target_dir}' != Current target_dir '{target_dir}'\n"
            "上次部署的目标目录与当前即将部署的目标目录不一致.\n"
            "本程序目前仅支持维护一个目标目录的部署, 如果需要更改目标目录, 请先执行`undeploy`(undo all apply)操作撤销上次部署.\n"
            "想要支持在多个目标目录部署的功能, 可以在本项目的issue中提出并支付一定酬劳, 或者自己实现.\n",
            file=sys.stderr,
        )
        exit(1)

    # 2.2 分析出 status.log 中的 APPLY_DIFF_DIR 文件夹, 得到 cur_diff_dirs
    cur_diff_dirs = []
    with open(f"{REPO_PATH}/.database/status.log", 'r') as f:
        lines = f.readlines()
        for line in lines:
            if line.startswith("APPLY_DIFF_DIR"):
                cur_diff_dirs.append(line.split()[1].strip("'\""))

    if not quiet:
        print("cur_diff_dirs:", cur_diff_dirs, '\n')

    tar_len = len(new_diff_dirs)
    cur_len = len(cur_diff_dirs)

    # 3. 对比 new_diff_dirs 和 cur_diff_dirs, 得到需要 apply 和 revert 的 diff 文件夹
    if new_diff_dirs == cur_diff_dirs:
        if not quiet:
            print("No need to deploy")
        return

    need_to_revert_dirs = []
    need_to_apply_dirs = []
    for i in range(min(tar_len, cur_len)):
        if new_diff_dirs[i] != cur_diff_dirs[i]:
            # 进行添加
            need_to_revert_dirs = cur_diff_dirs[i:]
            need_to_apply_dirs = new_diff_dirs[i:]
            break

    if tar_len > cur_len:
        need_to_apply_dirs = new_diff_dirs[cur_len:]
    elif tar_len < cur_len:
        need_to_revert_dirs = cur_diff_dirs[tar_len:]

    if not quiet:
        print("need_to_revert_dirs:", need_to_revert_dirs)
        print("need_to_apply_dirs:", need_to_apply_dirs, '\n')

    # 在 undo 和 deploy 之前, 先将 last-oper.log 中的历史记录清除
    log.clean_log_file("last-oper.log")

    # 更新元信息
    log.update_log_meta_info(new_image)

    for dir in need_to_revert_dirs:
        # undo_diff_dir(dir) # Alert! Arbitrary undo is not allowed.
        undo_last_apply(quiet)  # Just undo and undo again.

    for dir in need_to_apply_dirs:
        apply_diff_dir(dir, target_dir, quiet)

    # 更新 operations.log
    log.update_operations_log()

    # 更新 status.log
    log.update_status_log(include_undo=True)


# ------------------------------------------------------------
# Follow is the main function, entry point of the program
# ------------------------------------------------------------
def main(quiet=False):
    prompt()
    res = 0
    while True:
        try:
            res = input("> Please input your choice: (1~7) ")
            if int(res) in range(1, 8):
                break
            else:
                print("> Invalid input, please input again.")
        except ValueError:
            print("> Invalid input, please input again.")

    if res == '1':
        ensure_podman(quiet)
    elif res == '2':
        confirm_info_before_pull(quiet)
        ensure_image(IMAGE, quiet)
    elif res == '3':
        undo_all_apply(quiet)
    elif res == '4':
        confirm_info_befor_deploy(quiet)
        ensure_image(IMAGE, quiet)
        auto_deploy(IMAGE, TARGET_DIR, quiet)
    elif res == '5':
        confirm_info_befor_deploy(quiet)
        ensure_image(IMAGE, quiet)
        force_deploy(IMAGE, TARGET_DIR, quiet)
    elif res == '6':
        want_continue(quiet)
        undo_last_apply(independent=True, quiet=quiet)
    elif res == '7':
        log.clear_all_log_files(quiet)


def deploy(args: argparse.Namespace):
    """部署镜像"""
    # 将 args.target_dir 转换为绝对路径
    args.target_dir = os.path.abspath(os.path.expanduser(args.target_dir))
    running_logger.info(f"deploy 开始部署镜像{args.image} 到 {args.target_dir}")
    ensure_podman(args.quiet)

    ensure_image(args.image, args.quiet)

    if args.force:
        force_deploy(args.image, args.target_dir, args.quiet)
    else:
        auto_deploy(args.image, args.target_dir, args.quiet)


def undeploy(args: argparse.Namespace):
    """撤销部署"""
    running_logger.info("undeploy 开始撤销部署")
    undo_all_apply(args.quiet)


def info(args: argparse.Namespace):
    """查看部署信息"""
    running_logger.info("info 开始查看部署信息")
    print("    当前部署信息:")
    with open(f"{REPO_PATH}/.database/status.log", "r") as f:
        lines = f.readlines()
        for i in range(2):
            print("   ", lines[i].strip())


def clear(args: argparse.Namespace):
    """清除部署日志"""
    running_logger.info("clear 开始清除部署日志")
    log.clear_all_log_files(args.quiet)


def purge(args: argparse.Namespace):
    """删除podman拉取的所有镜像"""
    running_logger.info("purge 开始删除podman拉取的所有镜像")
    os.system("podman rmi -a")


def interactive(args: argparse.Namespace):
    """交互式操作"""
    running_logger.info("interactive 开始交互式操作")
    try:
        main(args.quiet)
    except (KeyboardInterrupt, EOFError) as e:
        running_logger.info("交互式操作被中断")
        print("Exit")
        exit(0)


if __name__ == '__main__':
    # 解析命令行参数
    # Create the top-level parser
    parser = argparse.ArgumentParser(
        description=DESCRIPTION, add_help=True, epilog=EPILOG
    )

    subparsers = parser.add_subparsers(
        # help='help-可以使用的子命令',
        title="子命令列表",
        # description="desc-本软件的子命令介绍信息如下: ",
        # prog="prog-用法信息",
    )

    # ------------------------------------------------------------
    # Add subparser for `deploy` sub-command.
    # ------------------------------------------------------------
    subparser_deploy = subparsers.add_parser(
        'deploy',
        help='部署镜像并记录部署日志',
        description="""deploy 命令用于将镜像部署在本机上, 并记录部署日志。
        部署分为两种模式:
        默认的自动模式以及使用 -f 或 --force参数开启的强制模式。
        使用后一种模式需要先仔细阅读用法信息(在脚本文件的开始部分)。否则后果自负！""",
        epilog=EPILOG,
    )

    subparser_deploy.add_argument(
        '--image',
        '-i',
        type=str,
        action='store',
        required=True,
        # nargs=1,
        help='''指定用于部署的镜像, 必选参数 (image to deploy)''',
    )
    subparser_deploy.add_argument(
        '--target-dir',
        '-t',
        type=str,
        action='store',
        help='部署操作的目标目录 (target dir), 默认为根目录 /',
        default="/",
    )
    subparser_deploy.add_argument(
        '--force',
        '-f',
        action='store_true',
        help='指定使用强制部署 (force deploy)模式, 默认为自动部署 (auto deploy)模式',
    )

    subparser_deploy.add_argument(
        '--quiet',
        '-q',
        action='store_true',
        help='不输出冗余的信息，不进行过多提问 (quiet)',
    )

    subparser_deploy.set_defaults(func=deploy)

    # ------------------------------------------------------------
    # Add subparser for `undeploy` sub-command.
    # ------------------------------------------------------------
    subparser_undeploy = subparsers.add_parser(
        'undeploy',
        help='根据日志文件撤销部署',
        description='''undeploy 命令用于撤销所有的部署, 没有任何选项。
        如需撤销单个server的部署。需要ssh进入单个server的命令行中进行操作。
        ''',
    )
    subparser_undeploy.add_argument(
        '--quiet',
        '-q',
        action='store_true',
        help='不输出冗余的信息，不进行过多提问 (quiet)',
    )
    subparser_undeploy.set_defaults(func=undeploy)

    # ------------------------------------------------------------
    # Add subparser for `info` sub-command.
    # ------------------------------------------------------------
    subparser_info = subparsers.add_parser(
        'info',
        help='显示部署信息',
        description='info 命令用于显示部署信息, 没有任何选项。',
    )
    subparser_info.set_defaults(func=info)

    # ------------------------------------------------------------
    # Add subparser for `clear` sub-command.
    # ------------------------------------------------------------
    subparser_clear = subparsers.add_parser(
        'clear',
        help='清除日志',
        description='clear 命令用于清除部署时产生的日志, 没有任何选项。',
    )
    subparser_clear.add_argument(
        '--quiet',
        '-q',
        action='store_true',
        help='不输出冗余的信息，不进行过多提问 (quiet)',
    )
    subparser_clear.set_defaults(func=clear)

    # ------------------------------------------------------------
    # Add subparser for `purge` sub-command.
    # ------------------------------------------------------------
    subparser_purge = subparsers.add_parser(
        'purge',
        help='清除所有镜像',
        description='purge 命令用于清除拉取的所有镜像, 以释放存储空间, 没有任何选项。',
    )
    subparser_purge.set_defaults(func=purge)

    # ------------------------------------------------------------
    # Add `--interactive` option.
    # ------------------------------------------------------------
    parser.add_argument('--interactive', '-i', action='store_true', help='交互式操作')

    # ------------------------------------------------------------
    # Add `--quiet` option.
    # ------------------------------------------------------------
    parser.add_argument(
        '--quiet', '-q', action='store_true', help='不输出冗余的信息，不进行过多提问 (quiet)'
    )

    if len(sys.argv) == 1:  # 如果没有提供任何参数, 则打印帮助信息
        parser.print_help()
        sys.exit()

    args = parser.parse_args()
    running_logger.info(args)
    if not args.quiet:
        print(f'{vars(args)=}')

    if 'func' in args:
        args.func(args)  # 执行子命令对应的函数
        exit()

    if args.interactive:
        interactive(args)
        exit()
